<?php

namespace App\Kuafu\Pay\Channel\FuYou;

use App\Kuafu\Base\Enum\BaseCacheEnum;
use App\Kuafu\Pay\Enum\PayCodeEnum;
use App\Kuafu\Pay\Exceptions\PayException;
use App\Kuafu\Pay\Model\PayStreamModel;
use App\Kuafu\Shop\Service\ShopAppletService;
use App\Support\Util;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\RequestOptions;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use KuafuComposer\ApiTime\Service\ApiTimeService;

class FuYouBase
{
    private        $mchntCd;
    private        $host;
    private        $payChannelFY;
    private        $notifyUrl;
    private        $mchntKey;
    private        $orderPrefix;
    private string $reservedFyTermId;
    private        $timeOut = 8;

    public static function getIns()
    {
        return new self;
    }

    /**
     * 加载商户配置文件
     * @param string $host
     * @param string $mchId
     * @param string $mchKey
     * @param string $notifyUrl
     * @param string $orderPrefix
     * @param string $reservedFyTermId 交易终端号
     * @return $this
     */
    public function loadMchConfig(
        string $host,
        string $mchId,
        string $mchKey,
        string $notifyUrl,
        string $orderPrefix,
        string $reservedFyTermId = ''
    ): self {
        $this->host             = $host;
        $this->mchntCd          = $mchId;
        $this->mchntKey         = $mchKey;
        $this->notifyUrl        = $notifyUrl;
        $this->orderPrefix      = $orderPrefix;
        $this->reservedFyTermId = $reservedFyTermId;

        return $this;
    }

    /**
     * 加载支付渠道配置
     * @param int $payChannel
     * @return $this
     * @throws PayException
     */
    public function loadChannel(int $payChannelChild)
    {
        switch ($payChannelChild) {
            case PayStreamModel::PAY_CHANNEL_CHILD_ALI:
                $this->payChannelFY = 'ALIPAY';
                break;
            case PayStreamModel::PAY_CHANNEL_CHILD_WX:
                $this->payChannelFY = 'WECHAT';
                break;
            case PayStreamModel::PAY_CHANNEL_CHILD_UNION_PAY:
                $this->payChannelFY = 'UNIONPAY';
                break;
            default:
                throw new PayException(PayCodeEnum::ERR_PAY_ERROR_PAY_CHANNEL_CHILD);
        }
        
        /*
            ALIPAY(统一下单、条码支付、服务窗支付)
            WECHAT(统一下单、条码支付，公众号支付)
            UNIONPAY
            WXAPP(微信app)
            ALIAPP(支付宝app)
            WXH5(微信h5)
            ALIH5(支付宝h5)
            WXBX(微信保险类)
            ALBX(支付宝保险类)
            WXXS(微信线上所有交易)
         */

        return $this;
    }

    /**
     * 条码支付，商户扫用户二维码收款(被扫)
     *
     * @param string $orderId
     * @param string $authCode
     * @param int $totalAmount
     * @param string $title
     * @param string $orderType
     */
    public function microPay(string $orderId, string $authCode, string $title, int $totalAmount): array
    {
        $params = [
            'version'        => '1.0',
            'mchnt_cd'       => $this->mchntCd,
            'term_id'        => '88888888',
            'random_str'     => self::createRandomStr(),
            'order_type'     => $this->payChannelFY,
            'goods_des'      => $title,
            'mchnt_order_no' => $orderId,
            'order_amt'      => $totalAmount,
            'term_ip'        => $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0',
            'txn_begin_ts'   => date('YmdHis', time()),
            'auth_code'      => $authCode,
            'notify_url'     => $this->notifyUrl,
        ];

        if ($this->reservedFyTermId) {
            $params['reserved_fy_term_id'] = $this->reservedFyTermId;
        }

        $params['sign'] = $this->createSign($params, 'mchnt_cd', 'order_type', 'order_amt', 'mchnt_order_no',
            'txn_begin_ts', 'goods_des', 'term_id', 'term_ip', 'auth_code', 'random_str', 'version');

        $result = $this->curlPost('/aggregatePay/micropay', $params);

        // 把服务不可用转成输密码的场景
        if (isset($result['result_code']) && $result['result_code'] == '010002') {
            $result['result_code'] = '030010';
        }

        return $result;
    }

    /**
     * 主扫统一下单
     *
     * @param string $orderId
     * @param string $totalAmount
     * @param string $title
     * @return array
     */
    public function qrPay(string $orderId, int $totalAmount, string $title): array
    {
        $params = [
            'version'        => '1.0',
            'mchnt_cd'       => $this->mchntCd,
            'term_id'        => '88888888',
            'random_str'     => self::createRandomStr(),
            'order_type'     => $this->payChannelFY,
            'goods_des'      => $title,
            'mchnt_order_no' => $orderId,
            'order_amt'      => $totalAmount,
            'term_ip'        => $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0',
            'txn_begin_ts'   => date('YmdHis', time()),
            'notify_url'     => $this->notifyUrl,
        ];
        if ($this->reservedFyTermId) {
            $params['reserved_fy_term_id'] = $this->reservedFyTermId;
        }

        $params['sign'] = $this->createSign($params, 'mchnt_cd', 'order_type', 'order_amt', 'mchnt_order_no',
            'txn_begin_ts', 'goods_des', 'term_id', 'term_ip', 'notify_url', 'random_str', 'version');

        $result = $this->curlPost('/aggregatePay/preCreate', $params);
        return $result;
    }

    /**
     * 退款申请
     *
     * @param string $orderId
     * @param string $refundOrderId
     * @param string $totalAmount
     * @param string $refundAmount
     * @param string $orderType
     * @return array
     */
    public function commonRefund(
        string $orderId,
        string $refundOrderId,
        int $totalAmount,
        int $refundAmount,
        int $operatorId = 0
    ) {
        $params = [
            'version'         => '1.0',
            // 必填 String 8 1.0
            // 必填 String 20 机构号,接入机构在富友的唯一代码
            'mchnt_cd'        => $this->mchntCd,
            // 必填 String 15 富友分配的商户号
            'term_id'         => '88888888',
            // 必填 String 8 终端号(没有真实终端号统一填88888888)
            'mchnt_order_no'  => $orderId,
            // 必填 String 30 商户原交易订单号, 商户系统内部的订单号（5到30 个字符、只能包含字母数字,区分大小写)
            'random_str'      => self::createRandomStr(),
            // 必填 String 32 随机字符串，不长于32 位
            'order_type'      => $this->payChannelFY,
            // 必填 String 20 订单类型:ALIPAY (统一下单、条码支付、服务窗支付), WECHAT(统一下单、条码支付，公众号支付),UNIONPAY,BESTPAY(翼支付)
            'refund_order_no' => $refundOrderId,
            // 必填 String 30 商户退款单号（5 到30 个字符、 只能包含字母数字或者下划线，区分大小写）
            'total_amt'       => $totalAmount,
            // 必填 Number 16 总金额
            'refund_amt'      => $refundAmount,
            // 必填 Number 16 退款金额
//            'operator_id'     => $operatorId,
            // 非必填 String 32 操作员
        ];


        $params['sign'] = $this->createSign($params, 'mchnt_cd', 'order_type', 'mchnt_order_no',
            'refund_order_no', 'total_amt', 'refund_amt', 'term_id', 'random_str', 'version');

        $result = $this->curlPost('/aggregatePay/commonRefund', $params);
        return $result;
    }

    /**
     * 退款查询
     * @param string $refundNo
     * @return array
     * @throws GuzzleException
     */
    public function refundQuery(string $refundNo)
    {
        $params         = [
            'version'         => '1.0',
            'mchnt_cd'        => $this->mchntCd,
            'random_str'      => self::createRandomStr(),
            'term_id'         => '88888888',
            'refund_order_no' => $refundNo,
        ];
        $params['sign'] = $this->createSign($params, 'mchnt_cd', 'refund_order_no', 'term_id', 'random_str', 'version');

        $result = $this->curlPost('/aggregatePay/refundQuery', $params);
        return $result;
    }

    /**
     * 关闭订单
     * 接口支持微信、支付宝各种下单类交易(order_type)的关闭。只有未支付的订单才能发起关闭。
     * 注意：订单生成后不能马上调用关单接口，最短调用时间间隔为 5 分钟
     * @param string $payNo
     * @return array
     * @throws GuzzleException
     */
    public function close(string $payNo)
    {
        $params = [
            'version'        => '1.0',
            'mchnt_cd'       => $this->mchntCd,
            'random_str'     => self::createRandomStr(),
            'term_id'        => '88888888',
            'mchnt_order_no' => $payNo,
            'order_type'     => $this->payChannelFY,
        ];

        $params['sign'] = $this->createSign($params, 'mchnt_cd', 'mchnt_order_no', 'order_type', 'term_id',
            'random_str', 'version');

        $result = $this->curlPost('/aggregatePay/closeOrder', $params);
        return $result;
    }

    /**
     * 撤销订单
     * 本接口支持微信、支付宝条码支付撤销。建议先发起查询确认订单状态未支付（或者支付中）后发起撤销。如果原交易成功，建议使用退款接口
     * 建议：原交易请求完至少 15S 后发起撤销。如果原交易是未支付，目前仅支持撤销当日交易
     * @param string $payNo
     * @param string $cancelNo
     * @return array
     * @throws GuzzleException
     */
    public function cancel(string $payNo, string $cancelNo)
    {
        $params = [
            'version'         => '1.0',
            'mchnt_cd'        => $this->mchntCd,
            'random_str'      => self::createRandomStr(),
            'term_id'         => '88888888',
            'mchnt_order_no'  => $payNo,
            'cancel_order_no' => $cancelNo,
            'order_type'      => $this->payChannelFY,
        ];

        $params['sign'] = $this->createSign($params, 'mchnt_cd', 'mchnt_order_no', 'order_type', 'cancel_order_no',
            'term_id', 'random_str', 'version');

        $result = $this->curlPost('/aggregatePay/cancelOrder', $params);
        return $result;
    }

    /**
     * 订单查询
     *
     * @param string $orderId
     */
    public function commonQuery($orderId)
    {
        $params         = [
            'version'        => '1.0',
            // 必填 String 8 1
            'mchnt_cd'       => $this->mchntCd,
            'term_id'        => '88888888',
            'order_type'     => $this->payChannelFY,
            // 必填 String 20 订单类型:ALIPAY (统一下单、条码支付、服务窗支付), WECHAT(统一下单、条码支付，公众号支付,小程序),UNIONPAY,BESTPAY(翼支付)
            'mchnt_order_no' => $orderId,
            'random_str'     => self::createRandomStr(),
        ];
        $params['sign'] = $this->createSign($params, 'mchnt_cd', 'order_type', 'mchnt_order_no', 'term_id',
            'random_str', 'version');

        $result = $this->curlPost('/aggregatePay/commonQuery', $params);
        return $result;
    }

    /**
     * 小程序支付
     * @param int $amount
     * @param string $payNo
     * @param string $appId
     * @return array
     * @throws GuzzleException
     */
    public function miniPay(int $amount, string $payNo, string $openId)
    {
        $appId  = ShopAppletService::getAppIdByHeaderShopId();
        $params = [
            'version'        => '1.0',
            'mchnt_cd'       => $this->mchntCd,
            'random_str'     => self::createRandomStr(),
            'order_amt'      => $amount,
            'mchnt_order_no' => $payNo,
            'txn_begin_ts'   => date('YmdHis', time()),
            'goods_des'      => '夸父小程序点餐',
            'term_id'        => '88888888',
            'term_ip'        => $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0',
            'notify_url'     => $this->notifyUrl,
            'sub_openid'     => $openId, // openid
            'sub_appid'      => $appId, // appid
//            'reserved_trade_type' => 'LPXS',
            'trade_type'     => 'JSAPI',
        ];

        if ($this->reservedFyTermId) {
            $params['reserved_fy_term_id'] = $this->reservedFyTermId;
        }

        $params['sign'] = $this->createSign($params, 'mchnt_cd', 'trade_type', 'order_amt', 'mchnt_order_no',
            'txn_begin_ts', 'goods_des', 'term_id', 'term_ip', 'notify_url', 'random_str', 'version');

        $result = $this->curlPost('/aggregatePay/wxPreCreate', $params);
        return $result;
    }

    /**
     * 生成sign
     * @param array $param
     * @param string ...$keys
     * @return string
     */
    private function createSign(array $param, string ...$keys): string
    {
        $str = '';
        foreach ($keys as $k) {
            $str .= $param[$k] . "|";
        }

        $str .= $this->mchntKey;

        return md5($str);
    }

    /**
     * post请求
     * @param string $path
     * @param array $params
     * @return array
     * @throws GuzzleException
     */
    private int $curlNum = 0;

    private function curlPost(string $path, array $params): array
    {
        $this->curlNum++;
        $startMicroTime = microtime(true);


        $uuid = $params['random_str'] ?? self::createRandomStr();

        Log::info('FuYou Request:', [
            'uuid' => $uuid,
            'url'  => $this->host . $path,
            $params
        ]);

        try {
            $response = (new Client([
                RequestOptions::TIMEOUT => $this->timeOut,
                RequestOptions::VERIFY  => false
            ]))->post($this->host . $path, $requestParam = ['json' => $params]);
        } catch (RequestException $e) {
            $errno = $e->getHandlerContext()['errno'] ?? 0;

            if ($errno == 56) {
                if ($path == '/aggregatePay/commonQuery' && $this->curlNum <= 3) {
                    Log::info("FuYou curlPost Ssl Err 重试中，第{$this->curlNum}次");
                    return $this->curlPost($path, $params);
                } else {

                    // SSL问题直接忽略，不报警了
                    Log::warning("SSL56 occur...path:{$path}");
                    Log::warning($e);
                    throw new \Exception($e, 0);
                }
            } else {

                // 正常抛出报警
                throw $e;
            }

        } finally {
            try {
                $consumeTime = ApiTimeService::calcMsInt($startMicroTime);
                (new ApiTimeService('fy'))->add($path, $startMicroTime);
            } catch (\Exception $e) {
                Log::warning($e);
            }
        }

        $s = json_decode($response->getBody()->getContents(), true) ?? [];
        Log::info('FuYou Response:', ['uuid' => $uuid, 'data' => $s, 'consumeTime' => $consumeTime ?? 0,]);

        $this->curlNum = 0;

        // 转换错误信息
        $s['ori_result_msg'] = $s['result_msg'];
        $s['result_msg']     = $this->convertErrMsg($s);

        return $s;
    }

    /**
     * 回调返回数据
     * @return string
     */
    public static function callbackReturn()
    {
        return json_encode(["result_code" => "000000", "result_msg" => "SUCCESS"]);
    }

    /**
     * 产生随机字符串，不长于32位
     * @param int $length
     * @return string
     */
    public static function createRandomStr($length = 32)
    {
        $string = "abcdefghijklmnopqrstuvwxyz0123456789";
        $result = '';
        for ($i = 0; $i < $length; $i++) {
            $result .= substr($string, mt_rand(0, strlen($string) - 1), 1);
        }

        return $result;
    }

    /**
     * 查询提现手续费信息
     * @param $amt
     * @return array
     * @throws GuzzleException
     */
    public function queryFeeAmt($amt)
    {
        $params         = [
            'mchnt_cd'   => $this->mchntCd,
            'amt'        => $amt,
            'random_str' => self::createRandomStr(),
        ];
        $params['sign'] = $this->createSign($params, 'mchnt_cd', 'amt', 'random_str');

        $result = $this->curlPost('/aggregatePay/queryFeeAmt', $params);
        return $result;
    }

    /**
     * 转换错误信息
     * @param array $s
     * @return string
     */
    private function convertErrMsg(array $s): string
    {
        $result_code = $s['result_code'] ?? '';
        $result_msg  = $s['result_msg'] ?? '';

        if (!$result_code || $result_code['000000']) {
            return $result_msg;
        }

        // 其他错误
        if ($result_code == '030001') {

            if (strpos($result_msg, '支付失败，获取顾客账户信息失败，请顾客刷新付款码后重新收款，如再次收') !== false) {
                $result_msg = '付款码长度错误，请重新配置扫码枪，或手动输入付款码。';
            }

            if (strpos($result_msg, '输入源“/body/payer/auth_code”映射到值字段“授') !== false) {
                $result_msg = '用户支付码有误，需要重新扫码支付。';
            }
        }

        return $result_msg;
    }
}
